Asian and lookback options are a type of exotic option which are classified as being strongly path dependent. This is due to the fact that their payoffs depend on the entire underlying's asset price path. As a results, Monte Carlo simulations are a good numerical solution choice for estimating their price. This report will use Monte Carlo simulations to calculate Asian and lookback option prices and provide analysis and insights into how these option prices change from variation in their initial conditions and numerical solution implementation.
Asian options give the holder a payoff which depends on the average price of the underlying over the life of the option. This averaging is subject to two characteristics; type which is either arithmetic or geometric and frequency which is either discrete or continuous.
\begin{array}{c|cc} \textbf{A} & Arithmetic & Geometric\\ \hline Discrete & \frac{1}{n}\sum_{i=1}^{n} S(t_i) & e^{\frac{1}{n} \sum_{i=1}^{n} log(S(t_i))} \\ Continuous & \frac{1}{T} \int_{0}^{T} S(t) dt & e^{\frac{1}{T} \int_{0}^{T} log(S(t)) dt} \\ \end{array}Here $i$ denotes a particular sampling date, $n$ is the total number of sampling dates and $T$ is the expiry date. Discrete frequencies subject the averaging to particular sampling dates, while continuous frequencies calculate an average from all observed prices. Additionally, there are two types of payoff variations, fixed strike and floating strike.
\begin{array}{c|cc} \textbf{Payoff} & Call & Put\\ \hline Fixed Strike & \max(A-K,0) & \max(K-A,0) \\ Floating Strike & \max(S_T-A,0) & \max(A - S_T,0) \\ \end{array}Lookback options give the holder a payoff which depends on the maximum or minimum price of the underlying over the life of the option. Similarly to Asian options, lookback options have two types of frequencies, discrete or continuous. The frequency determines when the maximum or minimum prices are observed.
\begin{array}{c|cc} \textbf{Frequency} & S_{min} & S_{max}\\ \hline Discrete & \min_{\forall i \in D}(S_i) & \max_{\forall i \in D}(S_i) \\ Continuous & \min_{0 \leq t \leq T}(S_t) & \max_{0 \leq t \leq T}(S_t) \\ \end{array}Here $D$ is the set of all sampling dates and $T$ is the expiry date. Lookback options also have two payoff variations, fixed strike and floating strike.
\begin{array}{c|cc} \textbf{Payoff} & Call & Put\\ \hline Fixed Strike & \max(S_{max}-K,0) & \max(K-S_{min},0) \\ Floating Strike & S_T-S_{min} & S_{max}-S_T \\ \end{array}The Euler-Maruyama method can be applied to stochastic differential equations (SDE) to find an approximation form. This method becomes helpful when deriving the risk-neutral random walk equation.
Consider the following SDE:
\begin{equation*} dX_t = a(X_t, t)dt + b(X_t, t)dW_t \end{equation*}Integrate both sides by a discrete time interval.
\begin{equation*} \int_{t_{n}}^{t_n + 1} dX_s = \int_{t_{n}}^{t_n + 1} a(X_s, s)ds + \int_{t_{n}}^{t_n + 1} b(X_s, s)dW_s \tag{1} \end{equation*}Then, the left hand integration rule is applied to each term to find its approximation.
\begin{equation*} \int_{t_{n}}^{t_n + 1} a(X_s, s)ds \approx a(X_n, n) \int_{t_{n}}^{t_n + 1} ds = a(X_n, n) \delta t \end{equation*}\begin{equation*} \int_{t_{n}}^{t_n + 1} b(X_s, s)dW_s \approx b(X_n, n) \int_{t_{n}}^{t_n + 1} dW_s = b(X_n, n) \Delta W_n \end{equation*}Here $n$ is the timestep and $\Delta W_n$ is normally distributed with a mean of zero and a standard deviation of $\sqrt{\delta t}$. Therefore, $\Delta W_n$ can be expressed as $\phi \sqrt{\delta t}$ with $\phi \sim \mathcal{N}(0, 1)$.
Substituting these into equation $(1)$ produces the Euler-Maruyama approximation.
\begin{equation*} X_{n+1} = X_n + a(X_n, t_n) \delta t + b(X_n, t_n) \phi \sqrt{\delta t} \end{equation*}It is important to remember that this Euler-Maruyama's has an accuracy of $O(\delta t^{1/2}$).
An options underlying can be modelled with the following Geometric Brownian Motion SDE: \begin{equation*} dS =\mu S dt + \sigma S dW \end{equation*}
Here the constants are $\mu$ is the percentage drift and $\sigma$ is the percentage volatility.
The Euler-Maruyama method is applied to the SDE by defining $a(S_t, t) = \mu S_t$ and $b(S_t, t) = \sigma S_t$.
\begin{equation*} S_{t+\delta t} \approx S_t + \mu S_t \delta t + \sigma S_t \phi \sqrt{\delta t} = S_t (1 + \mu\delta t + \sigma \phi \sqrt{\delta t}) \end{equation*}Under the risk-neutral measure $\mathbb{Q}$ all associated risk has been removed. Therefore, the percentage drift $\mu$ is only subject to the risk-free rate $r$. This defines the risk-neutral random walk equation.
\begin{equation*} S_{t+\delta t} = S_t (1 + r\delta t + \sigma \phi \sqrt{\delta t}) \tag{2} \end{equation*}The price of an option is defined by the present value of the option's payoff where the underlying follows a risk-neutral random walk.
\begin{equation*} V(S,t) = e^{-r(T-t)} \mathbb{E}^\mathbb{Q}[Payoff(S_T)] \end{equation*}Considering that the risk-neutral random walk simulations are randomly generated independent samples, the average option payoff across all simulations can estimate the option's expected payoff.
\begin{equation*} \mathbb{E}^\mathbb{Q}[Payoff(S_T)] \approx \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \end{equation*}Here $i$ denotes a particular simulated path and $n$ is the total number of simulations. By the law of large numbers, as $n \rightarrow \infty$ the estimated payoff converges to the actual payoff.
Thus, substituting back into the options price equation.
\begin{equation*} V(S,t) \approx e^{-r(T-t)} \frac{1}{n} \sum_{i=1}^n Payoff(S_T^i) \tag{3} \end{equation*}Firstly, the relevant modules which are used throughout the Jupyer Notebook are imported.
from enum import Enum
from itertools import product
from typing import Dict, Type, Optional
from functools import lru_cache
from abc import ABC, abstractmethod
from dataclasses import dataclass, asdict
import plotly
import numpy as np
import pandas as pd
from tqdm import tqdm
import plotly.io as pio
import plotly.graph_objects as go
from scipy.interpolate import griddata
from plotly.subplots import make_subplots
import plotly.express as px
pio.renderers.default='notebook'
plotly.offline.init_notebook_mode()
Next, the risk-neutral random walk equation $(2)$ is implemented into the function $\tt{simulate\_path}$. The function can generate a set of simulations by specifying the number of desired simulations in the variable $\tt{n\_sims}$. The python inbuilt functools function $\tt{lru\_cache}$ is added to $\tt{simulate\_path}$ as a decorator to cache simulated paths to memory. This becomes useful when simulated paths with the same input variables are called more than once. Additionally, the variance reduction technique antithetic variances has been added to the creation of standard normal random numbers. This will increase the accuracy of the Monte Carlo simulations by introducing negative dependencies on the draw of random numbers and create a true set of standard normal numbers which have a mean of zero and a standard deviation of one.
@lru_cache(maxsize=None)
def simulate_path(s0: float, r: float, vol: float, time_to_expiry: float, timesteps: int, n_sims: int, apply_var_reduction=True, seed_num=0) -> np.array:
np.random.seed(seed_num)
dt = time_to_expiry/timesteps
S = np.zeros((timesteps, n_sims))
S[0] = s0
for i in range(0, timesteps-1):
if apply_var_reduction:
w = np.random.standard_normal(n_sims//2)
w = np.concatenate((w,-w), axis=0)
else:
w = np.random.standard_normal(n_sims)
S[i+1] = S[i] * (1 + r * dt + vol * np.sqrt(dt) * w)
return S
As an example, 10 stock price simulations are generated with the input conditions of today's stock price $S_0 = 100$, constant risk-free interest rate $r = 0.05$, volatility $vol = 0.2$, $time\_to\_expiry = 1$ and $timesteps = 252$.
simulations = simulate_path(100, 0.05, 0.20, 1, 252, 10, apply_var_reduction=False)
fig = go.Figure([go.Scatter(x=list(range(0, 252)), y=sim, name=f'i={i+1}') for i, sim in enumerate(simulations.T)])
fig.update_layout(title='Monte Carlo Simulations',
xaxis_title='Timesteps',
yaxis_title='Asset Price',
width=700,
showlegend=False)
fig.show()
A few classes below are defined which help in investigating the affects on Asian and lookback option prices from changes in their input data. The Abstract Class $\tt{BaseOption}$ is the blueprint for the exotic options. This class defines the init constructor which most importantly calls $\tt{simulate\_path}$ and generates the option's simulated paths. In addition, Two Enums $\tt{AveragingType}$ and $\tt{StrikeType}$ hold information about the possible choices of averaging types and strike types and a dataclass $\tt{StrikeInfo}$ will hold the strike type and strike amount data.
class BaseOption(ABC):
"Abstract Class for Options"
def __init__(self, s0: float, r: float, vol: float, time_to_expiry: float, timesteps: int, n_sims: int):
self._s0 = s0
self._r = r
self._vol = vol
self._time_to_expiry = time_to_expiry
self._timesteps = timesteps
self._n_sims = n_sims
self._sims = simulate_path(s0, r, vol, time_to_expiry, timesteps, n_sims)
@abstractmethod
def price(self):
pass
class AveragingType(Enum):
Arithmetic = 1
Geometric = 2
class StrikeType(Enum):
Fixed = 1
Floating = 2
@dataclass(frozen=True, eq=True, unsafe_hash=True)
class StrikeInfo:
strike_type: StrikeType
strike: Optional[float] = None
def __post_init__(self):
if self.strike_type == StrikeType.Fixed and self.strike is None:
raise Exception("Fixed Strikes need a strike")
if self.strike_type == StrikeType.Floating and self.strike is not None:
raise Exception("Floating Strikes do not need a strike")
The $\tt{AsianOption}$ and $\tt{LookbackOption}$ classes hold the functionality which calculates the present value of the option's payoff defined in equation $(3)$. This report has only implemented continuous frequency types, further analysis could be done to investigate the differences in option prices between continuous and discrete frequency types.
class AsianOption(BaseOption):
"Class representing an Asian Option"
def __init__(self, averaging_type: AveragingType, strike_info: StrikeInfo, *args, **kwargs):
if not isinstance(strike_info, StrikeInfo):
raise ValueError(f"Unknown StrikeInfo {strike_info}")
if not isinstance(averaging_type, AveragingType):
raise ValueError(f"Unknown Averaging Type {averaging_type}")
super().__init__(*args, **kwargs)
self._averaging_type = averaging_type
self._strike_info = strike_info
def __fixed_strike(self, _sim_averages: np.array) -> Dict[str, float]:
call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(_sim_averages - self._strike_info.strike, 0))
put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._strike_info.strike - _sim_averages, 0))
return {'call': call, 'put': put}
def __floating_strike(self, _sim_averages: np.array) -> Dict[str, float]:
call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._sims[-1] - _sim_averages, 0))
put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(_sim_averages - self._sims[-1], 0))
return {'call': call, 'put': put}
def __fixed_strike_arithmetic(self) -> Dict[str, float]:
_sim_averages = self._sims.mean(axis=0)
return self.__fixed_strike(_sim_averages)
def __floating_strike_arithmetic(self) -> Dict[str, float]:
_sim_averages = self._sims.mean(axis=0)
return self.__floating_strike(_sim_averages)
def __fixed_strike_geometric(self) -> Dict[str, float]:
_sim_averages = np.exp(np.mean(np.log(self._sims), axis=0))
return self.__fixed_strike(_sim_averages)
def __floating_strike_geometric(self) -> Dict[str, float]:
_sim_averages = np.exp(np.mean(np.log(self._sims), axis=0))
return self.__floating_strike(_sim_averages)
def price(self):
mapping = {(AveragingType.Arithmetic, StrikeType.Fixed): self.__fixed_strike_arithmetic,
(AveragingType.Arithmetic, StrikeType.Floating): self.__floating_strike_arithmetic,
(AveragingType.Geometric, StrikeType.Fixed): self.__fixed_strike_geometric,
(AveragingType.Geometric, StrikeType.Floating): self.__floating_strike_geometric}
pricer = mapping.get((self._averaging_type, self._strike_info.strike_type))
return pricer()
class LookbackOption(BaseOption):
"Class representing an Lookback Option"
def __init__(self, strike_info: StrikeInfo, *args, **kwargs):
if not isinstance(strike_info, StrikeInfo):
raise ValueError(f"Unknown Strike Info {strike_info}")
super().__init__(*args, **kwargs)
self._strike_info = strike_info
self._sim_maxs = np.amax(self._sims, axis=0)
self._sim_mins = np.amin(self._sims, axis=0)
def __fixed_strike(self) -> Dict[str, float]:
call = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._sim_maxs - self._strike_info.strike, 0))
put = np.exp(-self._r*self._time_to_expiry) * np.mean(np.maximum(self._strike_info.strike - self._sim_mins, 0))
return {'call': call, 'put': put}
def __floating_strike(self) -> Dict[str, float]:
call = np.exp(-self._r*self._time_to_expiry) * np.mean(self._sims[-1] - self._sim_mins)
put = np.exp(-self._r*self._time_to_expiry) * np.mean(self._sim_maxs - self._sims[-1])
return {'call': call, 'put': put}
def price(self):
mapping = {StrikeType.Fixed: self.__fixed_strike, StrikeType.Floating: self.__floating_strike}
pricer = mapping.get(self._strike_info.strike_type)
return pricer()
price = AsianOption(averaging_type=AveragingType.Arithmetic, strike_info=StrikeInfo(StrikeType.Fixed, 100), s0=100, r=0.05, vol=0.20, time_to_expiry=1, timesteps=252, n_sims=10000).price()
f"Asian Option: Call Price = {price['call']:.2f} and Put Price {price['put']:.2f}"
'Asian Option: Call Price = 5.72 and Put Price 3.33'
price = LookbackOption(strike_info=StrikeInfo(StrikeType.Fixed, 100), s0=100, r=0.05, vol=0.20, time_to_expiry=1, timesteps=252, n_sims=10000).price()
f"Lookback Option: Call Price = {price['call']:.2f} and Put Price {price['put']:.2f}"
'Lookback Option: Call Price = 18.11 and Put Price 11.60'
The $\tt{Compare}$ class below creates an easy way to generate and compare a combination of BaseOption type classes. This is done by defining a list of possible values for each BaseOption variable. The init constructor then 1. produces a list of all possible option combinations, 2. initialises each combination into the specified BaseOption and 3. prices each of them.
class Compare:
def __init__(self, option: Type[BaseOption], **kwargs):
self.combinations = self.__get_combinations(**kwargs)
self.options = [option(**combination) for combination in tqdm(self.combinations)]
unraveled_combinations = [self.unravel_dataclass(x) for x in self.combinations]
self.df = pd.DataFrame(list(x | y for x, y in zip(unraveled_combinations, [option.price() for option in tqdm(self.options)])))
@staticmethod
def __get_combinations(**kwargs):
return [{list(kwargs)[i]: c for i, c in enumerate(prod)} for prod in product(*kwargs.values())]
@staticmethod
def unravel_dataclass(x):
strike_info = x.pop('strike_info')
return x | asdict(strike_info)
@staticmethod
def make_pretty(styler):
styler.set_table_styles([{'selector': 'caption', 'props': 'caption-side: Top; font-size: 1.25em'},
{'selector': 'th:not(.index_name)', 'props': 'text-align: center'}])
styler.background_gradient(axis=0, cmap='YlGnBu')
return styler
def table(self, table_name, row, cols, filter):
subset_df = self.df.copy()
for filter in filters:
subset_df = subset_df.query(filter)
df_pivot = subset_df.pivot(index=row, columns=cols, values=['call', 'put'])
return df_pivot.rename(columns={'call': 'Call Option', 'put': 'Put Option'}).style.pipe(self.make_pretty).set_caption(table_name)
def surface_plot(self, plot_name, filters, x_name, y_name):
subset_df= self.df.copy()
for filter in filters:
subset_df = subset_df.query(filter)
assert len(subset_df) != 0, f"DF is size zero {filter}"
for col in subset_df:
if col not in (x_name, y_name, "call", "put"):
assert len(subset_df[col].unique()) == 1, f"{col} does not have one unique value: {subset_df[col].unique()}"
x = np.array(subset_df[x_name])
y = np.array(subset_df[y_name])
z_call = np.array(subset_df["call"])
z_put = np.array(subset_df["put"])
xi = np.linspace(x.min(), x.max(), 100)
yi = np.linspace(y.min(), y.max(), 100)
X,Y = np.meshgrid(xi,yi)
scene = lambda z_name: dict(xaxis_title=x_name.replace("_", " ").capitalize(),
yaxis_title=y_name.replace("_", " ").capitalize(),
zaxis_title=z_name,
xaxis = dict(
backgroundcolor="rgb(200, 200, 230)",
gridcolor="white",
showbackground=True,
zerolinecolor="white"),
yaxis = dict(
backgroundcolor="rgb(230, 200, 230)",
gridcolor="white",
showbackground=True,
zerolinecolor="white"),
zaxis = dict(
backgroundcolor="rgb(230, 230, 200)",
gridcolor="white",
showbackground=True,
zerolinecolor="white"),
camera=dict(center=dict(x=0, y=0, z=0),
eye=dict(x=1.6, y=1.7, z=1.7)))
Z_call = griddata((x,y),z_call,(X,Y), method='cubic')
Z_put = griddata((x,y),z_put,(X,Y), method='cubic')
fig = make_subplots(rows=1, cols=2, horizontal_spacing=0, subplot_titles=['Call Option', 'Put Option'], specs=[[{'type': 'scene', 'is_3d': True}, {'type': 'scene', 'is_3d': True}]])
fig.add_trace(go.Surface(x=xi,y=yi,z=Z_call, colorbar_x=-0.07, colorscale=px.colors.sequential.Aggrnyl), row=1, col=1)
fig.add_trace(go.Surface(x=xi,y=yi,z=Z_put, colorscale=px.colors.sequential.Agsunset), row=1, col=2)
fig.update_annotations(y=0.9, selector={'text':'Call Option'})
fig.update_annotations(y=0.9, selector={'text':'Put Option'})
fig.update_layout(title=plot_name, scene = scene("Call"), scene2= scene("Put"),
width=800,
height=400,
margin=dict(
r=0, l=0,
b=0, t=70)
)
fig.show()
Two compare variables were generated, one for each option type with the varying option data defined below. The number of simulations and number of timesteps to use for each simulation was carefully considered. These variables are important because they affect the accuracy of the option prices and the performance of the Notebook. For this analysis, 252 timesteps (representing 252 business days in a year) and 10,000 simulations were chosen to find a middle ground between both trade-offs.
CompareAsianOptions = Compare(AsianOption,
s0=[100],
r=list(x/100 for x in range(0,55,5)),
vol=list(x/100 for x in range(20,120,20)),
time_to_expiry=list(x/100 for x in range(100,600,100)),
timesteps=[252],
n_sims=[10000],
strike_info=[StrikeInfo(StrikeType.Fixed, s) for s in range(20,220, 20)] + [StrikeInfo(StrikeType.Floating)],
averaging_type=[AveragingType.Geometric, AveragingType.Arithmetic])
CompareLookbackOptions = Compare(LookbackOption,
s0=[100],
r=list(x/100 for x in range(0,55,5)),
vol=list(x/100 for x in range(20,120,20)),
time_to_expiry=list(x/100 for x in range(100,600,100)),
timesteps=[252],
n_sims=[10000],
strike_info=[StrikeInfo(StrikeType.Fixed, s) for s in range(20,220, 20)] + [StrikeInfo(StrikeType.Floating)])
display(CompareAsianOptions.df.head(5))
display(CompareLookbackOptions.df.head(5))
100%|██████████| 6050/6050 [00:10<00:00, 586.89it/s] 100%|██████████| 6050/6050 [01:07<00:00, 89.01it/s] 100%|██████████| 3025/3025 [00:25<00:00, 119.38it/s] 100%|██████████| 3025/3025 [00:00<00:00, 11402.64it/s]
| s0 | r | vol | time_to_expiry | timesteps | n_sims | averaging_type | strike_type | strike | call | put | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | AveragingType.Geometric | StrikeType.Fixed | 20.0 | 79.658405 | 0.000000 |
| 1 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | AveragingType.Arithmetic | StrikeType.Fixed | 20.0 | 79.982099 | 0.000000 |
| 2 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | AveragingType.Geometric | StrikeType.Fixed | 40.0 | 59.658405 | 0.000000 |
| 3 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | AveragingType.Arithmetic | StrikeType.Fixed | 40.0 | 59.982099 | 0.000000 |
| 4 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | AveragingType.Geometric | StrikeType.Fixed | 60.0 | 39.658488 | 0.000083 |
| s0 | r | vol | time_to_expiry | timesteps | n_sims | strike_type | strike | call | put | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | StrikeType.Fixed | 20.0 | 95.924446 | 0.000000 |
| 1 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | StrikeType.Fixed | 40.0 | 75.924446 | 0.000000 |
| 2 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | StrikeType.Fixed | 60.0 | 55.924446 | 0.043856 |
| 3 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | StrikeType.Fixed | 80.0 | 35.924446 | 2.064148 |
| 4 | 100 | 0.0 | 0.2 | 1.0 | 252 | 10000 | StrikeType.Fixed | 100.0 | 15.924446 | 14.214183 |
The variables $\tt{CompareAsianOption}$ and $\tt{CompareLookbackOption}$ hold all combinations of Asian and lookback option prices. The changes in option prices can be analysed by filtering input data and creating surface plots or pivoted dataframes for the certain values that are being investigated. The base case filters were chosen to be the same as the initial pricing examples with $S_0 = 100$, risk-free interest rate $r = 0.05$, volatility $vol = 0.2$, and $time\_to\_expiry = 1$.
The following section provides analysis on the changes in risk-free rate, volatility and time to expiry against the strike and option prices. In addition, the differences between arithmetic and geometric averaging type and the differences between fixed and floating strikes is explored. To compare the changes across different options moneyness, the strikes are used for the y-axis on the surface plots and across table columns. For reference, the initial stock price has been set to $100$ for all prices, therefore for call options, $strikes \approx 100$ are at the money options (ATM), $strikes \gtrsim 100$ are out of the money (OTM) and $strikes \lesssim 100$ are in the money (ITM). While put options are the inverse, $strikes \lesssim 100$ are OTM and $strikes \gtrsim 100$ are ITM.
The risk-free rate impacts two parts of the price estimation
filters = ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Risk-free Rate - Base Case", filters, "r", "strike")
filters = ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Risk-free Rate - Base Case", 'r', ['strike'], filters))
filters = ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Risk-free Rate - Base Case", filters, "r", "strike")
filters = ["s0 == 100", "vol == 0.2", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Risk-free Rate - Base Case", 'r', ['strike'], filters))
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| r | ||||||||||
| 0.000000 | 79.982099 | 20.060640 | 4.559393 | 0.301332 | 0.000000 | 0.000000 | 0.078541 | 4.577294 | 20.319233 | 80.017901 |
| 0.050000 | 78.489019 | 21.456574 | 5.715737 | 0.488061 | 0.000000 | 0.000000 | 0.041320 | 3.325072 | 17.121984 | 73.707689 |
| 0.100000 | 77.028645 | 22.759914 | 6.989720 | 0.758219 | 0.000047 | 0.000000 | 0.021514 | 2.348069 | 14.213316 | 67.745389 |
| 0.150000 | 75.600353 | 23.968631 | 8.346293 | 1.130816 | 0.000493 | 0.000000 | 0.010756 | 1.602577 | 11.601260 | 62.113416 |
| 0.200000 | 74.203523 | 25.085389 | 9.762459 | 1.617613 | 0.000911 | 0.000000 | 0.005711 | 1.057397 | 9.287165 | 56.794309 |
| 0.250000 | 72.837535 | 26.112463 | 11.206506 | 2.238982 | 0.001302 | 0.000000 | 0.002975 | 0.673034 | 7.281526 | 51.771893 |
| 0.300000 | 71.501773 | 27.054146 | 12.646702 | 2.996926 | 0.001864 | 0.000000 | 0.001467 | 0.410387 | 5.576975 | 47.031006 |
| 0.350000 | 70.195627 | 27.915217 | 14.060112 | 3.884495 | 0.003089 | 0.000000 | 0.000875 | 0.239532 | 4.157677 | 42.557557 |
| 0.400000 | 68.918490 | 28.699796 | 15.427044 | 4.903967 | 0.004735 | 0.000000 | 0.000509 | 0.134158 | 3.017482 | 38.337453 |
| 0.450000 | 67.669761 | 29.412399 | 16.731608 | 6.036087 | 0.008659 | 0.000000 | 0.000327 | 0.072099 | 2.129141 | 34.359403 |
| 0.500000 | 66.448845 | 30.057210 | 17.964353 | 7.247447 | 0.015911 | 0.000000 | 0.000205 | 0.037960 | 1.451668 | 30.611971 |
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| r | ||||||||||
| 0.000000 | 95.924446 | 35.924446 | 15.924446 | 4.076422 | 0.023151 | 0.000000 | 2.064148 | 14.214183 | 34.214183 | 94.214183 |
| 0.050000 | 94.212180 | 37.138415 | 18.113826 | 5.553327 | 0.046307 | 0.000000 | 1.317869 | 11.597731 | 30.622320 | 87.696085 |
| 0.100000 | 92.887049 | 38.596804 | 20.500056 | 7.361569 | 0.095033 | 0.000000 | 0.817203 | 9.423784 | 27.520532 | 81.810777 |
| 0.150000 | 91.916198 | 40.273719 | 23.059560 | 9.516093 | 0.184350 | 0.000000 | 0.492377 | 7.642549 | 24.856709 | 76.499187 |
| 0.200000 | 91.242619 | 42.118774 | 25.744159 | 11.988607 | 0.341271 | 0.000000 | 0.286271 | 6.205870 | 22.580485 | 71.704331 |
| 0.250000 | 90.809143 | 44.081096 | 28.505080 | 14.727649 | 0.601280 | 0.000000 | 0.161081 | 5.053608 | 20.629624 | 67.357671 |
| 0.300000 | 90.572904 | 46.123811 | 31.307447 | 17.675221 | 1.012546 | 0.000000 | 0.086160 | 4.138518 | 18.954883 | 63.403976 |
| 0.350000 | 90.490567 | 48.209281 | 34.115519 | 20.764490 | 1.616103 | 0.000000 | 0.046001 | 3.409475 | 17.503236 | 59.784522 |
| 0.400000 | 90.525989 | 50.306786 | 36.900385 | 23.935166 | 2.471254 | 0.000000 | 0.024148 | 2.829644 | 16.236045 | 56.455248 |
| 0.450000 | 90.644854 | 52.387165 | 39.634602 | 27.134445 | 3.659359 | 0.000000 | 0.012667 | 2.365974 | 15.118537 | 53.376226 |
| 0.500000 | 90.822620 | 54.430780 | 42.300167 | 30.305644 | 5.193231 | 0.000000 | 0.006573 | 1.993778 | 14.124391 | 50.516231 |
Volatility has a positive relationship to both call and put options. The volatility affects the magnitude of the randomness term in the risk-neutral walk equation, thus creating more variation in the underlying's asset price. \begin{equation*} S_{t+\delta t} = S_t (1 + r \delta t + \color{red}{\boldsymbol{\sigma}} \phi \sqrt{\delta t}) \end{equation*} This intake produces higher expected payoffs, with ATM options impacted more than ITM and OTM options. The driver for this, is the fact that ATM options have the highest amount of extrinsic value - the uncertainty if the option will expire ITM or OTM. Therefore, when the randomness factor is increased there is a higher possibility that the option will expire at a higher average payoff. This is best seen in the surface plots below, as both options have a curve at the strike 100 and volatility 20% point which becomes more straight as the volatility increases. Interestingly, lookback options are more sensitive to the changes in volatility as their payoff equations are subject to the maximum or minimum of the underlying's price. This payoff structure leverages increased volatility well, as it creates a higher possibility that there will be a higher or lower underlying price. Asian options however, have payoff equations that are subject to the averaging of the underlying price which consequently reduces some of the incorporated volatility.
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Volatility - Base Case", filters, "vol", "strike")
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Volatility - Base Case",'vol', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Volatility - Base Case", filters, "vol", "strike")
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Volatility - Base Case", 'vol', ['strike'], filters))
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| vol | ||||||||||
| 0.200000 | 78.489019 | 21.456574 | 5.715737 | 0.488061 | 0.000000 | 0.000000 | 0.041320 | 3.325072 | 17.121984 | 73.707689 |
| 0.400000 | 78.436757 | 22.671810 | 10.025228 | 3.629354 | 0.101453 | 0.000000 | 1.308819 | 7.686824 | 20.315539 | 73.861404 |
| 0.600000 | 78.351975 | 25.165575 | 14.302827 | 7.748781 | 1.113225 | 0.000000 | 3.887365 | 12.049206 | 24.519749 | 74.957958 |
| 0.800000 | 78.239141 | 28.149876 | 18.506939 | 12.093286 | 3.486271 | 0.000000 | 6.984501 | 16.366152 | 28.977087 | 77.443838 |
| 1.000000 | 78.107491 | 31.293659 | 22.605007 | 16.457675 | 6.821390 | 0.001050 | 10.260983 | 20.596919 | 33.474176 | 80.911657 |
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| vol | ||||||||||
| 0.200000 | 94.212180 | 37.138415 | 18.113826 | 5.553327 | 0.046307 | 0.000000 | 1.317869 | 11.597731 | 30.622320 | 87.696085 |
| 0.400000 | 111.439250 | 54.365484 | 35.340896 | 20.706412 | 3.874470 | 0.000121 | 9.079875 | 23.823471 | 42.848059 | 99.921825 |
| 0.600000 | 130.612736 | 73.538970 | 54.514382 | 39.148525 | 15.264108 | 0.036036 | 18.264444 | 34.664276 | 53.688865 | 110.762630 |
| 0.800000 | 151.818275 | 94.744509 | 75.719921 | 60.004180 | 32.091807 | 0.374664 | 26.959439 | 44.187327 | 63.211916 | 120.285681 |
| 1.000000 | 175.147787 | 118.074021 | 99.049433 | 83.165520 | 52.768909 | 1.254851 | 34.768165 | 52.486830 | 71.511418 | 128.585184 |
When analysing the changes in time to expiry, it's important to remember the Euler-Maruyama methods accuracy of $O(\delta t ^{1/2})$. This is because the $\delta t$ term is calculated by dividing the time to expiry by the timesteps. This will cause longer dated options to be less accurate than shorter dated options. With the maximum $time\_to\_expiry = 5$ having an accuracy of $\sqrt{5/252} \approx 0.141$.
The time to expiry affects
With the lifetime of the option extended, the impact on drift, volatility and discounting factor becomes more prevalent. This broadly creates higher option prices as the drift and volatility terms have a stronger positive affect on higher expected payoffs.
For ITM options however there can be situations where there is a negative relationship. This is due the volatility term being less impactful for ITM options resulting in the drift and discounting factor terms contributing to most of the price changes. As explained in the the volatility analysis, ITM options are less sensitive to volatility as their extrinsic value is low. This also applies in this case, as the volatility term of the risk-neutral random walk equation is scaled higher as the time to expiry increases. Although, the key point here is that it does not scaled at the same rate as the drift and discount factor terms. Thus for this example, this negative relationship is driven by the following factors:
Note that this is also subject to the options payoff type. Although this does not affect lookback options as much as Asian options because the lookback option payoff equations are more sensitive to volatility.
This is best seen through a comparison between the below two Asian Option tables, the first shows prices for a risk-free rate at 5% (base case) and the second shows them with a risk-free rate at 0%. With the risk-free rate set to zero, the only contributors to change in price is the volatility. This demonstrates that the drift and discounting factor are the main contributors to these ITM options being less valuable as the time to expiry increases.
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic"]
CompareAsianOptions.surface_plot("Asian Option - Time to Expiry - Base Case", filters, "time_to_expiry", "strike")
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Time to Expiry - Base Case", 'time_to_expiry', ['strike'], filters))
filters = ["s0 == 100", "r == 0.0", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "averaging_type == @AveragingType.Arithmetic", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Option - Time to Expiry - Risk-free Rate 0%", 'time_to_expiry', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed"]
CompareLookbackOptions.surface_plot("Lookback Option - Time to Expiry - Base Case", filters, "time_to_expiry", "strike")
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareLookbackOptions.table("Lookback Option - Time to Expiry - Base Case", 'time_to_expiry', ['strike'], filters))
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| time_to_expiry | ||||||||||
| 1.000000 | 78.489019 | 21.456574 | 5.715737 | 0.488061 | 0.000000 | 0.000000 | 0.041320 | 3.325072 | 17.121984 | 73.707689 |
| 2.000000 | 77.011391 | 22.941827 | 8.628474 | 2.014509 | 0.007885 | 0.000000 | 0.220681 | 4.004076 | 15.486860 | 67.770481 |
| 3.000000 | 75.566482 | 24.343974 | 10.994174 | 3.768279 | 0.061669 | 0.000000 | 0.419971 | 4.284330 | 14.272595 | 62.208463 |
| 4.000000 | 74.153660 | 25.618043 | 13.034442 | 5.536842 | 0.225261 | 0.000000 | 0.588228 | 4.379243 | 13.256257 | 57.068522 |
| 5.000000 | 72.772295 | 26.765020 | 14.838337 | 7.242681 | 0.535216 | 0.000000 | 0.720772 | 4.370105 | 12.350464 | 52.371047 |
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| time_to_expiry | ||||||||||
| 1.000000 | 79.982099 | 20.060640 | 4.559393 | 0.301332 | 0.000000 | 0.000000 | 0.078541 | 4.577294 | 20.319233 | 80.017901 |
| 2.000000 | 79.964296 | 20.459736 | 6.436585 | 1.187458 | 0.004170 | 0.000000 | 0.495439 | 6.472289 | 21.223161 | 80.039874 |
| 3.000000 | 79.946597 | 21.013936 | 7.870449 | 2.161585 | 0.023833 | 0.000000 | 1.067339 | 7.923852 | 22.214988 | 80.077236 |
| 4.000000 | 79.929006 | 21.602826 | 9.074695 | 3.105416 | 0.077270 | 0.000000 | 1.673820 | 9.145689 | 23.176410 | 80.148264 |
| 5.000000 | 79.911527 | 22.191957 | 10.131602 | 3.998224 | 0.176321 | 0.000000 | 2.280430 | 10.220075 | 24.086696 | 80.264793 |
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| time_to_expiry | ||||||||||
| 1.000000 | 94.212180 | 37.138415 | 18.113826 | 5.553327 | 0.046307 | 0.000000 | 1.317869 | 11.597731 | 30.622320 | 87.696085 |
| 2.000000 | 99.481806 | 45.191560 | 27.094812 | 13.286580 | 1.013639 | 0.000000 | 3.069144 | 14.387420 | 32.484169 | 86.774414 |
| 3.000000 | 103.230242 | 51.587764 | 34.373604 | 20.396151 | 3.387704 | 0.000000 | 4.323990 | 15.787333 | 33.001493 | 84.643972 |
| 4.000000 | 106.176906 | 57.053060 | 40.678445 | 26.888528 | 6.752410 | 0.000000 | 5.206194 | 16.509897 | 32.884512 | 82.008357 |
| 5.000000 | 108.605498 | 61.877451 | 46.301435 | 32.862687 | 10.774766 | 0.000141 | 5.820044 | 16.832312 | 32.408328 | 79.136375 |
Geometric averages have a compounding affect which makes them smaller than arithmetic averages. This can be seen in the price difference between geometric and arithmetic Asian options. For fixed strike call options the arithmetic averaged options are higher than their geometric counterparts. This is expected since the payoff equation $\max(A-K,0)$ increases if the $A$ term increases. The inverse happens for put options as a higher $A$ term in the payoff equation $\max(K-A,0)$ produces lower payoffs. The opposite happens for floating strike options as the $A$ term is reversed in the payoff formulas. This affect is less observable when the risk-free rate, volatility and time to expiry values are at the base case (with some comparisons within the Euler-Maruyama accuracy) however it does become more observable at higher values. An example is show in the tables below.
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Fixed Strike Option: Geometric vs Arithmetic - Base Case", 'averaging_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "vol == 0.2", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Floating"]
display(CompareAsianOptions.table("Asian Floating Strike Option: Geometric vs Arithmetic - Base Case", 'averaging_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "vol == 0.6", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Fixed", "strike in [20, 80, 100, 120, 180]"]
display(CompareAsianOptions.table("Asian Fixed Strike Option: Geometric vs Arithmetic - Vol 60%", 'averaging_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "vol == 0.6", "time_to_expiry == 5", "timesteps == 252", "n_sims == 10000", "strike_type == @StrikeType.Floating"]
display(CompareAsianOptions.table("Asian Floating Strike Option: Geometric vs Arithmetic - Vol 60%", 'averaging_type', ['strike'], filters))
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| averaging_type | ||||||||||
| AveragingType.Geometric | 71.129708 | 25.262228 | 13.532204 | 6.192702 | 0.296443 | 0.000000 | 0.860567 | 4.706559 | 12.943073 | 53.774860 |
| AveragingType.Arithmetic | 72.772295 | 26.765020 | 14.838337 | 7.242681 | 0.535216 | 0.000000 | 0.720772 | 4.370105 | 12.350464 | 52.371047 |
| Call Option | Put Option | |
|---|---|---|
| strike | nan | nan |
| averaging_type | ||
| AveragingType.Geometric | 17.335326 | 4.242826 |
| AveragingType.Arithmetic | 16.094025 | 4.644111 |
| Call Option | Put Option | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| strike | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 | 20.000000 | 80.000000 | 100.000000 | 120.000000 | 180.000000 |
| averaging_type | ||||||||||
| AveragingType.Geometric | 60.132212 | 27.913260 | 21.842116 | 17.202869 | 8.860955 | 0.185568 | 14.694664 | 24.199536 | 35.136304 | 73.522437 |
| AveragingType.Arithmetic | 72.285507 | 36.995185 | 30.165265 | 24.915248 | 15.054311 | 0.017588 | 11.455313 | 20.201409 | 30.527408 | 67.394518 |
| Call Option | Put Option | |
|---|---|---|
| strike | nan | nan |
| averaging_type | ||
| AveragingType.Geometric | 40.072375 | 15.982880 |
| AveragingType.Arithmetic | 33.620926 | 21.852706 |
To analyse the price differences between fixed and floating strike options, only ATM fixed strike options are compared with their floating strike option equivalent.
Similar to the geometric and arithmetic analysis, arithmetic Asian options at the base case values have a minimal difference between floating strikes and fixed strike prices. The affects become slightly more noticeable at higher values with the floating strike type call and put option prices higher than ATM fixed strike options. The main driver for floating legs having an increased price might be due to the payoff equations having the $S_T$ term, because this term is produced by the risk-neutral random walk equation it is subject to drift and volatility. This inherently will cause the average payoffs to be higher. However, it needs to be mentioned that in these cases most of the comparisons are within the Euler-Maruyama accuracy making the results uncertain. These prices should be re-run with a higher number of simulations and increased timesteps to increase the accuracy of the results.
Geometric Asian options, on the other hand have a wider difference between results, with floating strike call options having a higher price than ATM fixed strike call options and floating strike put options having a smaller price than ATM fixed strike put options. These findings appear to be similar to the geometric and arithmetic results. Considering geometric means are lower than arithmetic means and they are less affect by outliers, it would cause the payoff equations to be higher for call options and lower for put options.
ATM lookback fixed strike calls options are more valuable than floating strikes and floating strike put options are more valuable than ATM fixed strikes. This might be caused by the maximum and minimum terms not having a symmetric distance from $S_0$. Under the risk-neutral random walk equation, the drift term contributes to a higher maximum underlying price, while this works in the opposite direction for minimum prices.
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Arithmetic", "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Arithmetic - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Arithmetic", "vol == 0.6", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Arithmetic - Fixed vs Floating - Vol 60%", 'strike_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Geometric", "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Geometric - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "averaging_type == @AveragingType.Geometric", "vol == 0.6", "strike.isna() | strike == 100"]
display(CompareAsianOptions.table("Asian Option Geometric - Fixed vs Floating - Vol 60%", 'strike_type', ['strike'], filters))
filters = ["s0 == 100", "r == 0.05", "time_to_expiry == 1", "timesteps == 252", "n_sims == 10000", "vol == 0.2", "strike.isna() | strike == 100"]
display(CompareLookbackOptions.table("Lookback Option - Fixed vs Floating - Base Case", 'strike_type', ['strike'], filters))
| Call Option | Put Option | |||
|---|---|---|---|---|
| strike | nan | 100.000000 | nan | 100.000000 |
| strike_type | ||||
| StrikeType.Fixed | nan | 5.715737 | nan | 3.325072 |
| StrikeType.Floating | 5.766320 | nan | 3.320301 | nan |
| Call Option | Put Option | |||
|---|---|---|---|---|
| strike | nan | 100.000000 | nan | 100.000000 |
| strike_type | ||||
| StrikeType.Fixed | nan | 14.302827 | nan | 12.049206 |
| StrikeType.Floating | 14.544228 | nan | 12.087852 | nan |
| Call Option | Put Option | |||
|---|---|---|---|---|
| strike | nan | 100.000000 | nan | 100.000000 |
| strike_type | ||||
| StrikeType.Fixed | nan | 5.505044 | nan | 3.439906 |
| StrikeType.Floating | 5.969047 | nan | 3.197501 | nan |
| Call Option | Put Option | |||
|---|---|---|---|---|
| strike | nan | 100.000000 | nan | 100.000000 |
| strike_type | ||||
| StrikeType.Fixed | nan | 12.673742 | nan | 13.237845 |
| StrikeType.Floating | 15.977713 | nan | 10.703613 | nan |
| Call Option | Put Option | |||
|---|---|---|---|---|
| strike | nan | 100.000000 | nan | 100.000000 |
| strike_type | ||||
| StrikeType.Fixed | nan | 18.113826 | nan | 11.597731 |
| StrikeType.Floating | 16.434415 | nan | 13.277142 | nan |
This report applied the Euler-Maruyama scheme to the Geometric Brownian motion SDE and under the risk-neutral measure $\mathbb{Q}$ defined the risk-neutral random walk equation. This equation was then implemented in Python Juypter Notebooks to produce Monte Carlo simulations of option underlying stock prices. These simulations were used to calculate expected prices for Asian and lookback options by discounting the option's expected payoffs back to present day. Analysis was then conducted to provide insights into how different input data affected the option prices. Most interestingly, the changes in option prices greatly depend on the moneyness of the option with generally ATM options being affected more by changes in input data than ITM and OTM options. Lookback options were also found to have higher prices and be more sensitive to changes than compared with Asian options. Additionally, the report briefly looked into price differences between Asian and lookback averaging types and strike types. It is important to consider these different variations as they can affect option prices in different ways.